﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace DynamicMVVM
{
    public class DynamicModel : DynamicObject, INotifyPropertyChanged
    {
        private object _model;

        readonly Dictionary<string, PropertyInfo> _properties = new Dictionary<string, PropertyInfo>();

        public DynamicModel()
        {
            ModelState = new ModelStateDictionary();
        }

        public DynamicModel(object model)
            :this()
        {
            _model = model;
            SetModel(_model);
        }

        public object Model
        {
            get { return _model; }
            set
            {
                _model = value;
                SetModel(_model);
            }
        }

        public bool IsDirty { get; private set; }

        public ModelStateDictionary ModelState { get; private set; }

        private void SetModel(object model)
        {
            _properties.Clear();
            model.GetType().GetProperties().ToList().ForEach(x => _properties.Add(x.Name, x));
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (_properties.ContainsKey(binder.Name))
            {
                var info = _properties[binder.Name];

                result = info.GetValue(_model, null);

                return true;
            }
            return base.TryGetMember(binder, out result);
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            if (_properties.ContainsKey(binder.Name))
            {
                var info = _properties[binder.Name];

                ValidatePropertyValue(binder.Name, info, value);

                SetValue(info, value);

                NotifyPropertyChanged(binder.Name);

                IsDirty = true;

                NotifyPropertyChanged("IsDirty");

                return true;
            }
            return base.TrySetMember(binder, value);
        }

        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return _properties.Keys;
        }


        #region Implementation of INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
        protected void RaisePropertyChanged<T>(Expression<Func<T>> property)
        {
            if (PropertyChanged == null) return;

            var expression = property.Body as MemberExpression;
            if (expression == null) return;
            var member = expression.Member;
            PropertyChanged(this, new PropertyChangedEventArgs(member.Name));
        }

        #endregion

        #region Private methods

        private void SetValue(PropertyInfo info, object value)
        {
            var converter = TypeDescriptor.GetConverter(info.PropertyType);

            if (converter != null)
            {
                if (IsValid(info, converter, value))
                {
                    try
                    {
                        info.SetValue(_model, value, null);
                    }
                    catch (Exception)
                    {
                        var strValues = converter.ConvertToString(value);
                        if (strValues != null)
                        {
                            var newValue = converter.ConvertFromString(strValues);
                            info.SetValue(_model, newValue, null);
                        }
                    }
                }
                else
                {
                     throw new InvalidDataException();
                }
            }
            else
            {
                throw new InvalidDataException();
            }
        }

        private static bool IsValid(PropertyInfo info, TypeConverter converter, object value)
        {
            return info.PropertyType == value.GetType() || converter.IsValid(value);
        }

        private void ValidatePropertyValue(string propertyName, PropertyInfo info, object value)
        {
            var properyName = GetPropertyDisplayName(propertyName, info);

            var validations = info.GetCustomAttributes(typeof(ValidationAttribute), true)
                .Select(x => x as ValidationAttribute);

            var InvalidDataValues =
                validations.Select(x => x.GetValidationResult(value, GetValidationContext(properyName)))
                    .Where(x => x != ValidationResult.Success);

            if (InvalidDataValues.Any())
            {
                foreach (var validationResult in InvalidDataValues)
                {
                    ModelState.AddModelError(propertyName, validationResult.ErrorMessage);
                }
                throw new InvalidDataException(InvalidDataValues.FirstOrDefault().ErrorMessage);
            }
        }

        private ValidationContext GetValidationContext(string name)
        {
            return new ValidationContext(this, null, null)
            {
                DisplayName = name
            };
        }

        private static string GetPropertyDisplayName(string propertyName, PropertyInfo info)
        {
            var displayNameAttrs = info.GetCustomAttributes(typeof(DisplayAttribute), true)
                .Select(x => x as DisplayAttribute);
            if (displayNameAttrs.Any())
            {
                propertyName = displayNameAttrs.FirstOrDefault().GetName();
            }
            return propertyName;
        }

        #endregion
    }

    public class DynamicModel<T> : DynamicModel
        where T :class
    {
        public DynamicModel()
        {
            
        }

        public DynamicModel(T model)
            :base(model)
        {
            
        }

        public new T   Model
        {
            get { return base.Model as T; }
            set
            {
                base.Model  = value;
            }
        }
    }
}
